就是類別將內部在使用的方法和屬性讓外部可以取得,然後這個壞味道可能會產生一些問題 :
下面為一個不好的範例,它就是將 balance 公開出去,而且可以被修改。
class BankAccount {
// balance 是公開屬性,外部可以直接存取和修改
public balance: number;
constructor(initialBalance: number) {
this.balance = initialBalance;
}
// 存款方法
public deposit(amount: number): void {
this.balance += amount;
}
// 提款方法
public withdraw(amount: number): void {
this.balance -= amount;
}
}
// 外部誤用
account.balance = 5000;
這裡我有看過不少人這樣做過,這個方法如果為了測試而公開,他就有可能被外部誤用。所以這裡如果看到這個問題,通常會給 1 個建議 :
測試包含這個方法的公有方法。
然後我有看過一種情況就是,因為原本使用的公有方法太複雜,例如 checkout 結帳用的方法,然後將所有結帳前與後,要做的整間公司 domain 都放在裡面,所以導致他太巨大,然後要在裡面新加一個功能,結果就是要測試時,需要產很多資料,所以有不少人就會想說直接測試那個方法。
老實說,我自已也做過這個事情,因為光要產生那個方法所需要的資料,我都不知道要花多少時間了,因為太多資料關聯性要理解,所以在 deadline 與疲勞的雙重壓力下就改成 public 了。
但事實上問題的根源就只是那個方法職責太多了
,所以建議下次看到這個問題,如果你真的有 deadline 壓力,那我可能睜一隻眼閉一隻眼,但是我一定要在那個地方看到TODO 與 task 票
喔 ~
~小心得~
有時後也不要很強硬的 code review 時看到這種就要求直接拆與重構。
因為很多情況下有 deadline 且可能很多地方都不是那個人寫的,只是他運氣不好要改那裡,所以這裡以我 tech leader 角色,通常不強硬要求,但至少會做到上面說的開 task追蹤重構。
至於團隊有沒有重構處理那張票的流程,讓程式碼 be better,就真的是 tech leader 要想辦法與 PM、營運們協商導入流程了。
例如下面範例的 getUserFromDatabaseById,如果這時多加個 cache 怎麼是不是又要改了呢 ?
class UserService {
// 它暴露了內部的存儲細節,是使用 database
public getUserFromDatabaseById(id: number): { userId: number; userName: string; } | null {
return user ? { userId: user.id, userName: user.name } : null;
}
}
// 使用範例
const userService = new UserService();
console.log(userService.getUserFromDatabaseById(1));
大概如下程式碼,這個方法內部使用 http client 來取得到相關的資料,方法名沒有曝露實作細節,但是問題是出在回傳曝露他是使用 http,因為他回傳了 httpCode,如果這時改成用 db 的話,那是不是外面使用 httpCode 來驗證的地方就要全改了 ?
class UserService {
private httpClient: HttpClient;
constructor(httpClient: HttpClient) {
this.httpClient = httpClient;
}
// 方法內部使用 httpClient 來獲取用戶數據,並且回傳 httpCode,這暴露了實作細節
public async getUserById(id: number): Promise<{ user: User | null, httpCode: number }> {
try {
const response = await this.httpClient.get(`/users/${id}`);
if (response.status === 200) {
const userData = response.data;
return {
user: new User(userData.id, userData.name),
httpCode: response.status
};
} else {
return { user: null, httpCode: response.status };
}
} catch (error) {
return { user: null, httpCode: 500 };
}
}
}
這個原則的概念為如下,它又有另一個名稱叫最少知識原則
。
一個類別對其它類別應該知道的越少越好
然後比較工程師的說,就是一個物件只能與以下的東西進行溝通:
然後的重點就是 :
不要使用
自已耦合的物件
裡面的東西
備註: 但還是有特例,例如 proxy 的 class,他本身就是個代理,你叫他不能用依賴物件的方法也很奇怪。
因為如果可以使用耦合的物件裡面的東西,然後裡面又再使用裡面的東西,那是不是那條耦合線
就不斷的拉長,所以這個限制就是要將那條線給砍斷。
如下範例程式碼。
// Bad
class Address {
constructor(public city: string) {}
}
class Customer {
constructor(public address: Address) {}
}
class Order {
constructor(public customer: Customer) {}
// 違反 Demeter 法則的情況
public getCustomerCity(): string {
// 這裡跨越了多個對象的邊界
return this.customer.address.city;
}
}
// Good
class Address {
constructor(public city: string) {}
}
class Customer {
constructor(private address: Address) {}
// 提供一個方法,讓外部訪問 city,而不是直接暴露 address
public getCity(): string {
return this.address.city;
}
}
class Order {
constructor(private customer: Customer) {}
// 符合 Demeter 法則
public getCustomerCity(): string {
// 直接從 customer 取得 city,不需要了解其內部結構
return this.customer.getCity();
}
}
本篇文章中,咱們討論了以下五個看點 :
這 5 個看點事實上主要都是討論 :
類別的封裝
一個好的封裝有幾個特點 :
減少外部的依賴
與細節的變動,所影響的變動
。這個說來說去又是 SRP 這張圖,好的封裝就是可以降低左邊用戶的變動源。